接下來我們瞭解一下如果想要「回復先前狀態」要怎麼做。這裡會有點複雜,因為這些動作牽涉到工作目錄、暫存區及儲存庫的交互作用及狀態變化。「狀態改變」的意思,其實是對工作目錄和暫存區進行操作,去改變它們的內容,儲存庫一般而言只會新增而不會刪除,這些操作基本上會用到 git checkout
和 git reset
這兩個指令,昨天我們在 git status
中已經看過 Git 給我們的提示,使用到了這兩個指令:
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
deleted: README
modified: Rakefile
new file: new-file.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib/simplegit.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
no-commit.txt
我們先看中間 Changes not staged for commit
的部分,這是指這個檔案已經被 Git 納管,而目前檔案內容有更改,但還沒有用 git add
將它的更改狀態放到暫存區去。這裡說如果你要放棄工作目錄中的改變,那麼可以使用 git checkout -- <file>
這個指令。這裡它會將暫存區裡的檔案內容,拿來更改工作目錄中的檔案內容。但我們不是還沒將它的更改狀態放到暫存區去嗎?因此這裡暫存區的內容,其實是上一次提交時的內容。所以當你把上一次提交時的內容拿來更新工作目錄中的內容,就等於是放棄對這個檔案進行的更動了。這裡可以進行一個實驗,將一個檔案修改後先用 git add
將它的更改放到暫存區,再對它進行一次修改。此時對這個檔案使用 git checkout -- <file>
指令,會發現這個檔案的內容是回復到 git add
後的狀態,而不是上次提交後的狀態,因此可以證實 git checkout -- <file>
是用暫存區的內容來更改工作目錄的內容。
接下來進一步延伸,如果我們現在想將工作目錄中的某個檔案,回復到某一個提交的狀態,這時可以用 git checkout <commit> <file>
這個指令。
commit ca82a6dff817ec66f44342007202690a93763949 (HEAD -> master, origin/master, origin/HEAD)
Author: Scott Chacon <schacon@gmail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the verison number
Rakefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gmail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test code
lib/simplegit.rb | 5 -----
1 file changed, 5 deletions(-)
在上面的例子中,Rakefile
在 ca82a6dff817ec66f44342007202690a93763949
這個提交中作了修改。因為代表提交的 hash 字串很長,所以之後會用前 6 個字元來代替。假設希望將 Rakefile
回復到 085bb3
這個提交中的狀態,可以使用 git checkout 085bb3 Rakefile
。指令裡使用的 hash 字串也可以用前幾個字元代替,如果這幾個字元無法決定唯一的提交,Git 會告知。這個指令會從儲存庫中把所指定的提交的檔案內容拿出來,更新目前的暫存區和工作目錄內容。接下來用 git status
來檢查目前工作目錄的狀態,會發現 Rakefile
被放在 Changes to be committed
的狀態下,這是因為對於目前最近的一次提交 (ca82a6
) 而言,這個檔案的暫存區內容和工作目錄內容都改變了,成為 085bb3
的狀態。如果我們想要一口氣將目前工作目錄和暫存區的「所有檔案」都回復到某個特定提交的狀態,可以使用 git checkout <commit> .
指令。請注意最後的 .
是表示所有檔案的意思,這個 .
一定要加,否則會有完全不同的結果。
那如果我們現在在工作目錄新增一個檔案,在還沒使用 git add
將它納管的狀況下,使用 git checkout <commit> .
將工作目錄中所有檔案都回復到之前的狀態,那麼這個新增的檔案還會在工作目錄中嗎?答案是會。因為在我們要回復的提交當時的儲存庫中並沒有這個檔案,沒有東西可以從儲存庫拿出來去改變這個新檔案的內容,所以它不會受到影響。
看完 git checkout
後,我們來看一下 Changes to be committed
這一部分的 git reset
指令。git reset HEAD <file>
在有檔案作為參數的狀況下,會將暫存區以目前提交儲存庫中的狀態更新。HEAD
指的是目前的這個提交,之後會再說明。所以在 git add
某個有更改過的檔案後,暫存區的內容也跟著改變了,這個指令會讓暫存區的內容回到原本還沒有更改的狀態,所以叫作 unstage。
剛才提到 HEAD
,在介紹之前要先說明另一個重要的觀念。我們再回到「狀態更改」這件事。打個比方,假設你現在受雇於一家人,你的工作就是幫這家人做飯,而且要持續記錄追蹤這家人每餐吃了什麼。今天晚上這家人忽然說很想吃昨天晚上吃到的那些菜,該怎麼做呢?第一個方法是看看昨天晚上吃了什麼,然後今天晚上準備完完全全一模一樣的菜色。第二個方法,是帶他們坐時光機回到昨天晚上,就可以再吃一次昨晚的菜色了。回到過去之後會發生什麼?可能接下來吃的餐點,會跟原先版本產生些微的不同,有點像是「平行宇宙」的概念,人生就從那個時間點叉開了。
在平行宇宙的例子中,時光機的作用是帶我們在時間軸中穿梭,它同時也有標示的作用,告訴我們「現在」在整個時間軸的那個位置。時光機在 Git 裡就是 HEAD
這個指標的作用。看一下前面 git log
的範例,會發現在最近的一個提交,hash 字串旁有 HEAD -> master, origin/master, origin/HEAD
的標示,這表示 HEAD
目前位於這一個提交(關於這裡出現的 master
、origin
,之後會再說明)。在大部分的狀況下,HEAD
都是指向最近的一個提交,但它其實是可以移動的。將它移回較早的提交,就有點像是坐時光機回到之前某個時間點的感覺。git reset
這個指令,其中一個用法就是讓我們將 HEAD
指標移到某個特定的提交。實際上它除了移動 HEAD
指標,還會移動 HEAD
所指向的分支指標,分支指標待會會說明。它的基本用法是 git reset <commit>
。以 simplegit-progit
為例,下面是將此專案克隆 (clone) 回來後直接使用 git log --decorate
的輸出。HEAD
目前位於 ca82a5
提交。
commit ca82a6dff817ec66f44342007202690a93763949 (HEAD -> master, origin/master, origin/HEAD)
Author: Scott Chacon <schacon@gmail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the verison number
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gmail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test code
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gmail.com>
Date: Sat Mar 15 10:31:28 2008 -0700
first commit
我們使用 git rest 085bb3
指令,再用 git log --decorate
看看:
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 (HEAD -> master)
Author: Scott Chacon <schacon@gmail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test code
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gmail.com>
Date: Sat Mar 15 10:31:28 2008 -0700
first commit
我們會發現 HEAD
跑到 085bb3
這個提交了,表示我們現在位於 085bb3
,原本的 ca82a6
不見了。其實它不是不見,而是對目前的提交而言,它是一個「未來」才會發生的提交,所以 git log
看不到。以這裡的情況來說,如果想要看到原本 ca82a6
這個提交,可以在 git log
加上 --all
的選項。git reset <commit>
除了將 HEAD 指標移到指定的提交外,其實還有一個選項用來控制暫存區以及工作目錄是否跟著更動,在不加參數的狀況下,只有暫存區會隨著 HEAD
移動而更新,但工作目錄中的檔案是不會變的。因此以這裡的例子而言,此時使用 git status
會發現在 ca82a6
提交中修改的 Rakefile
,它的狀態是 Changes not staged for commit
。在工作目錄中 Rakefile
處於 ca82a6
提交的狀態,但在 index 中它的狀態處於 085bb3
提交,因此是已修改但尚未加到 index。
那如果我們移動 HEAD
之後,修改檔案再進行提交會發生什麼事呢?這時平行宇宙就會發生了,從旁觀者的角度來看,我們的提交記錄會從 085bb3
這個提交開始岔開。例如我們現在將 Rakefile
加入 index 再進行提交,然後使用 git log --all --graph --decorate
,會產生以下結果:
* commit bcbe1e432521e7339d77a9f1ccbbcf9085d014c4 (HEAD -> master)
| Author: vagrant <vagrant@example.com>
| Date: Mon Oct 8 23:29:24 2018 +0000
|
| changed the version number
|
| * commit ca82a6dff817ec66f44342007202690a93763949 (origin/master, origin/HEAD)
|/ Author: Scott Chacon <schacon@gmail.com>
| Date: Mon Mar 17 21:52:11 2008 -0700
|
| changed the verison number
|
* commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
| Author: Scott Chacon <schacon@gmail.com>
| Date: Sat Mar 15 16:40:33 2008 -0700
|
| removed unnecessary test code
|
* commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gmail.com>
Date: Sat Mar 15 10:31:28 2008 -0700
first commit
從左邊的圖會發現在 085bb3
提交後叉開了,在原本的 ca82a6
又多了一個分支,這個新分支有一個提交 bcbe1e
,就是我們剛才進行的提交,可以發現 HEAD
現在跑到這個提交了。其實 bcbe1e
這個提交和 ca82a6
的提交檔案內容是一樣的,但提交時會考慮一些其他的因素,因此它們的 hash 算出來是不同的。現在我們用 git reset ca82a6
把 HEAD
移回 ca82a6
此提交。
今天關於「狀態回復」就說明到這裡,明天來看一下分支 (branch) 的操作。